﻿using System;
using System.Collections.Generic;
using System.Collections;
using System.Linq;
using System.Text;
using System.IO;

namespace TharpPhoneBookConsoleLab.Model
{
    #region Name

    /// <summary>
    /// Represents a name. Cannot contain spaces.
    /// </summary>
    public struct Name
    {
        public readonly String DisplayText;

        public Name(String name)
        {
            DisplayText = VerifyName(name);
        }//end Name(String)

        private static String VerifyName(String name)
        {
            if (name.Contains(" "))
                throw new NameException("Names cannot contain spaces:\"" + name + "\"");
            return name;
        }//end String VerifyName(String)

        public class NameException : ArgumentException
        {
            public NameException(String message) : base(message) { }
            public NameException(String message, Exception innerException)
                : base(message, innerException) { }
            public NameException(String message, String paramName)
                : base(message, paramName) { }
            public NameException(String message, String paramName, Exception innerException)
                : base(message, paramName, innerException) { }
        }//end class PhoneNumberException
    }//end struct Name

    #endregion

    #region PhoneNumber

    /// <summary>
    /// Represents a phone number as a string.
    /// </summary>
    public struct PhoneNumber
    {
        public readonly string DisplayText;

        public PhoneNumber(string number)
        {
            DisplayText = number;
        }//end PhoneNumber

        //public static PhoneNumber NewPhoneNumber(string numberString)
        //{
        //    if (numberString.All(c => char.IsDigit(c)))
        //        return new PhoneNumber(numberString);
        //    throw new PhoneNumberException("PhoneNumbers cannot contain non-numeric characters:" + numberString);
        //}//end PhoneNumber NewPhoneNumber(string)

        class PhoneNumberException : ArgumentException
        {
            public PhoneNumberException(String message) : base(message) { }
            public PhoneNumberException(String message, Exception innerException)
                : base(message, innerException) { }
            public PhoneNumberException(String message, String paramName)
                : base(message, paramName) { }
            public PhoneNumberException(String message, String paramName, Exception innerException)
                : base(message, paramName, innerException) { }
        }//end class PhoneNumberException

    }//end struct PhoneNumber

    #endregion

    #region PhoneBookEntry

    /// <summary>
    /// Represents a phone book entry as a Name and a PhoneNumber.
    /// </summary>
    public class PhoneBookEntry
    {
        public Name Name { get; private set; }
        public PhoneNumber Number { get; private set; }

        public PhoneBookEntry(Name name, PhoneNumber number)
        {
            this.Name = name;
            this.Number = number;
        }//end PhoneBookEntry

        public PhoneBookEntry(string name, string number)
            : this(new Name(name), new PhoneNumber(number)) { }

        /// <summary>
        /// Creates a PhoneBookEntry out of a string if possible.
        /// </summary>
        /// <param name="possibleEntry">A string representing a name and a PhoneNumber</param>
        /// <param name="entry">out parameter assigned to the created entry</param>
        /// <returns>True if an entry was sucessfully created, otherwise false</returns>
        public static bool ValidateEntry(string possibleEntry, out PhoneBookEntry entry)
        {
            entry = default(PhoneBookEntry);

            var entryPieces = possibleEntry.Split();
            if (entryPieces.Length != 2) 
                return false;
            try {
                var name = new Name(entryPieces[0]);
                var number = new PhoneNumber(entryPieces[1]);
                entry = new PhoneBookEntry(name,number);
                return true;
            } catch (Exception ex) {                
                return false;
            }//end try-catch
        }//end bool ValidateEntry(string)

    }//end struct PhoneBookEntry

    #endregion

    #region PhoneBook

    public class PhoneBook : ICollection<PhoneBookEntry>
    {
        #region Fields

        private Dictionary<Name, PhoneNumber> _PhoneBook = new Dictionary<Name, PhoneNumber>();

        #endregion

        #region Public Interface

        /// <summary>
        /// Attempts to load a list of phone book entries from a file
        /// using AddOrReplace per file line. This will raise the
        /// OnPhoneBookUpdate event for every entry successfully loaded.
        /// Always succeeds.
        /// </summary>
        /// <param name="fileName">The file to load from</param>
        /// <param name="errorMessage">
        /// out parameter that contains any error messages produced
        /// while trying to load from the file
        /// </param>
        /// <returns>Returns true if no errors were raised</returns>
        public bool LoadDataFromFile(string fileName, out string errorMessage)
        {
            errorMessage = String.Empty;
            try
            {
                using (var file = new StreamReader(fileName))
                {
                    string fileLine = file.ReadLine();
                    int line = 1;
                    while (fileLine != null)
                    {
                        PhoneBookEntry entry;

                        if (!PhoneBookEntry.ValidateEntry(fileLine, out entry))
                        {
                            errorMessage = "Invalid entry on line " + line.ToString() + ".";
                            return false;
                        }//end if
                        this.AddOrReplace(entry);
                        fileLine = file.ReadLine();
                        line++;
                    }//end while fileLine
                }//end using file
            }
            catch (Exception ex)
            {
                errorMessage = "Could not load file \"" + fileName + "\": " + ex.Message;
                return false;
            }//end try-catch file
            return true;
        }//end LoadDataFromFile

        /// <summary>
        /// Adds an entry to the phonebook, or replaces the number
        /// of an existing entry. Raises OnPhoneBookUpdate.
        /// Always succeeds.
        /// </summary>
        /// <param name="entry">The entry to add or replace</param>
        public void AddOrReplace(PhoneBookEntry entry)
        {
            _PhoneBook[entry.Name] = entry.Number;
            Update();
        }//end AddOrReplace

        /// <summary>
        /// Checks to see if a name is contained in the phonebook
        /// </summary>
        /// <param name="name">The name to lookup</param>
        /// <returns>True if the name is in the phonebook, otherwise false</returns>
        public bool ContainsName(Name name)
        {
            return _PhoneBook.ContainsKey(name);
        }//end ContainsName

        /// <summary>
        /// Checks to see if a phone number is contained in the phonebook
        /// </summary>
        /// <param name="number">The number to lookup</param>
        /// <returns>True if the number is in the phonebook, otherwise false</returns>
        public bool ContainsNumber(PhoneNumber number)
        {
            return _PhoneBook.ContainsValue(number);
        }//end ContainsNumber

        /// <summary>
        /// Checks to see if an entry exists in the phonebook for a given name.
        /// If it does, the entry is stored in an out parameter.
        /// </summary>
        /// <param name="name">The name to look up</param>
        /// <param name="entry">Either the entry found, or null</param>
        /// <returns>True if an entry was found, otherwise false</returns>
        public bool TryFindEntry(Name name, out PhoneBookEntry entry)
        {
            try
            {
                entry = new PhoneBookEntry(name, _PhoneBook[name]);
                return true;
            }
            catch (KeyNotFoundException ex)
            {
                entry = null;
                return false;
            }//end try-catch
        }//end TryFindEntry

        /// <summary>
        /// Gets an entry from the phonebook for a given name.
        /// Throws KeyNotFoundException if no entry for the name exists.
        /// </summary>
        /// <param name="name">The name to look up</param>
        /// <returns>The entry found</returns>
        public PhoneBookEntry GetEntry(Name name) {
            PhoneBookEntry entry;
            if (TryFindEntry(name, out entry))
                return entry;
            throw new KeyNotFoundException("Could not find the name in the phone book: " + name.ToString());
        }//end GetEntry

        /// <summary>
        /// Gets the number associated with a name in the phonebook.
        /// Throws KeyNotFoundException if no entry for the name exists.
        /// </summary>
        /// <param name="name">The name to look up</param>
        /// <returns>The PhoneNumber for the given name</returns>
        public PhoneNumber GetNumber(Name name)
        {
            return GetEntry(name).Number;
        }//end GetNumber

        /// <summary>
        /// Attempts to store the contents of the phonebook in a file.
        /// Terminates on the first error raised.
        /// Always returns.
        /// </summary>
        /// <param name="fileName">The file to write to</param>
        /// <param name="errorMessage">An output parameter containing the first error message raised.</param>
        /// <returns>True if all entries were stored with no errors.</returns>
        public bool StoreDataInFile(string fileName, out string errorMessage)
        {
            errorMessage = String.Empty;

            try
            {
                using (var file = new StreamWriter(fileName))
                {
                    foreach (var entry in this)
                    {
                        file.WriteLine(entry.Name.DisplayText + " " + entry.Number.DisplayText);
                    }//end foreach entry

                }//end using file

                return true;
            }
            catch (Exception ex)
            {
                errorMessage = "Could not save to file \"" + fileName + "\": " + ex.Message;
                return false;
            }//end try-catch file

        }//end StoreDataInFile

        #region OnPhoneBookUpdate [event]

        /// <summary>
        /// Delegate type which represents a handler of the OnPhoneBookUpdate event.
        /// </summary>
        /// <param name="sender">The PhoneBook object that was updated</param>
        /// <param name="e">A blank System.EventArgs object</param>
        public delegate void OnPhoneBookUpdateHandler(PhoneBook sender, System.EventArgs e);

        /// <summary>
        /// Event which is raised everytime the contents of the phonebook changes.
        /// </summary>
        public event OnPhoneBookUpdateHandler OnPhoneBookUpdate;

        private void Update()
        {
            if (OnPhoneBookUpdate != null)
                OnPhoneBookUpdate(this, new System.EventArgs());
        }//end Update()

        #endregion

        #region ICollection Members

        public IEnumerator<PhoneBookEntry> GetEnumerator()
        {
            foreach (var entry in _PhoneBook)
                yield return new PhoneBookEntry(entry.Key, entry.Value);
        }//end GetEnumerator

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }//end IEnumerable.GetEnumerator

        public bool IsReadOnly { get { return false; } }
        
        public int Count { get { return _PhoneBook.Count; } }

        public bool Remove(PhoneBookEntry entry)
        {
            return this.Contains(entry) && _PhoneBook.Remove(entry.Name);
        }//end Remove

        public void CopyTo(PhoneBookEntry[] array, int arrayIndex)
        {
            if (array == null)
                throw new ArgumentNullException("array");
            if (arrayIndex < 0)
                throw new ArgumentOutOfRangeException("arrayIndex");
            if (this.Count > (array.Length - arrayIndex))
                throw new ArgumentException("array is not large enough");
            int i = arrayIndex;
            foreach (var entry in this)
            {
                array[i] = entry;
                i++;
            }
        }//end CopyTo

        public bool Contains(PhoneBookEntry entry)
        {
            return _PhoneBook.ContainsKey(entry.Name) && _PhoneBook[entry.Name].Equals(entry.Number);
        }//end Contains

        public void Clear()
        {
            _PhoneBook.Clear();
        }//end Clear

        public void Add(PhoneBookEntry entry)
        {
            this.AddOrReplace(entry);
        }//end Add



        #endregion

        #endregion
    }//end class PhoneBook

    #endregion

}//end namespace TharpPhoneBookConsoleLab.Model
